/*
 * Copyright 2006 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.spectaql.internal.util;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.*;

/**
 * <p>
 * <code>ClassUtils</code> provides basic utility methods for dealing with classes and introspection.
 * </p>
 */
public class ClassUtils
{
    /**
     * <p>
     * Construct a {@link ClassUtils} instance.
     * </p>
     */
    private ClassUtils()
    {
        super();
    }

    /**
     * <p>
     * Get a method based solely upon its name. Since the parameter types are ignored, this method is only useful for
     * testing.
     * </p>
     *
     * @param clazz the class.
     * @param name the method name.
     * @return {@link Method} instance.
     * @throws IllegalStateException if more than one method with the given name is found.
     * @throws NoSuchMethodException if the method is not found.
     */
    public static Method getMethodByName(final Class<?> clazz, final String name) throws IllegalStateException,
            NoSuchMethodException
    {
        final Method[] methods = clazz.getMethods();
        Method result = null;
        for (final Method method : methods)
        {
            if (name.equals(method.getName()))
            {
                if (null != result)
                {
                    throw new IllegalStateException(String.format(
                            "Multiple methods with name %s were found on class %s.", name, clazz));
                }
                result = method;
            }
        }
        if (null == result)
        {
            throw new NoSuchMethodException(String.format("No method named %s was found on class %s.", name, clazz));
        }
        return result;
    }

    /**
     * <p>
     * Get the name of the package to which a given class belongs.
     * </p>
     *
     * @param clazz the class.
     * @return {@link String} package name.
     */
    public static String getPackageName(final Class<?> clazz)
    {
        // Get the fully qualified class name. 
        final String className = clazz.getName();
        final int lastDotIndex = className.lastIndexOf('.');
        final String result;
        if (-1 == lastDotIndex)
        {
            // This should only happen for primitives.
            result = null;
        }
        else
        {
            // Package name is everything up to the last dot.
            result = className.substring(0, lastDotIndex);
        }
        return result;
    }

    /**
     * <p>
     * Get a map of property names to {@link PropertyDescriptor} instances for a given class. {@link Introspector} is
     * used to collect the raw data, so all normal JavaBean functionality is supported (such as <code>BeanInfo</code>
     * classes.
     * </p>
     * 
     * @param clazz the JavaBean class.
     * @return {@link Map} of {@link String} property names to {@link PropertyDescriptor} instances.
     */
    public static Map<String, PropertyDescriptor> getPropertyDescriptorMap(final Class<?> clazz)
    {
        final Map<String, PropertyDescriptor> result;
        try
        {
            // Get the raw property descriptor array.
            final BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
            final PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();

            // Build a map of property names to descriptors.
            result = CollectionUtils.toMap(Arrays.asList(propertyDescriptors), new Extractor<String, PropertyDescriptor>()
            {
                public String extract(PropertyDescriptor object)
                {
                    return object.getName();
                }
            });
        }
        catch (IntrospectionException e)
        {
            throw new RuntimeException(String.format("%s getting bean info for class %s: %s.", e.getClass(), clazz,
                    e.getMessage()), e);
        }
        return result;
    }

    /**
     * <p>
     * Convert an array of fully qualified class names to a {@link Set} containing the corresponding {@link Class}
     * objects <em>for only those classes which are available on the runtime classpath.</em>
     * </p>
     * 
     * @param classNames the class names.
     * @return {@link Set} of {@link Class} instances.
     */
    public static Set<Class<?>> getAvailableClasses(final String... classNames)
    {
        final Set<Class<?>> result;
        if (0 == classNames.length)
        {
            result = Collections.emptySet();
        }
        else
        {
            result = new HashSet<Class<?>>(classNames.length);
            for (final String className : classNames)
            {
                try
                {
                    result.add(Class.forName(className));
                }
                catch (ClassNotFoundException e)
                {
                    // Just skip this class.
                }
            }
        }
        return result;
    }

    /**
     * <p>
     * Determine whether a given method is present on a class. The declaring class for the method must be the given
     * given class or one of its superclasses.
     * </p>
     *
     * @param method the method to check.
     * @param clazz the class to check.
     * @return <code>boolean</code> <code>true</code> if the method is present on the class.
     */
    public static boolean isMethodPresentOnClass(final Method method, final Class<?> clazz)
    {
        final boolean result;
        final Class<?> declaringClass = method.getDeclaringClass();
        if (declaringClass.equals(clazz))
        {
            // Method is declared directly on the check class.
            result = true;
        }
        else if (!clazz.isAssignableFrom(declaringClass))
        {
            // Method's declaring class is not assignable to the check class, so this cannot be a match.
            result = false;
        }
        else
        {
            // Method's declaring class is assignable to the check class. Examine the method signature.
            final String name = method.getName();
            final Class<?>[] parameterTypes = method.getParameterTypes();
            boolean foundMethod = false;
            try
            {
                // Remember to use the return value from getMethod() to ensure that it cannot be optimized away.
                final Method clazzMethod = clazz.getMethod(name, parameterTypes);
                foundMethod = null != clazzMethod;
            }
            catch (NoSuchMethodException e)
            {
                // Ignore and return false.
            }
            result = foundMethod;
        }
        return result;
    }
}
